﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace Xceed.Silverlight.Data
{
  internal static class QueryableExtensions
  {
    #region REFLECTION METHODS

    internal static MemberInfo GetMemberInfo( Type type, string memberName )
    {
      MemberInfo[] members = type.FindMembers( MemberTypes.Property | MemberTypes.Field,
        BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance,
        Type.FilterNameIgnoreCase, memberName );

      if( members.Length != 0 )
        return members[ 0 ];

      return null;
    }

    #endregion REFLECTION METHODS

    #region ORDERING

    internal static IQueryable OrderBy(
      this IQueryable queryable,
      IEnumerable<SortDescription> implicitSortDescriptions,
      IEnumerable<SortDescription> explicitSortDescriptions,
      bool reverse )
    {
      IEnumerable<string> propertyNames = new string[ 0 ];
      IEnumerable<ListSortDirection> sortDirections = new ListSortDirection[ 0 ];

      if( explicitSortDescriptions != null )
      {
        propertyNames = propertyNames.Concat( from e in explicitSortDescriptions
                                              select e.PropertyName );
        sortDirections = sortDirections.Concat( from e in explicitSortDescriptions
                                                select e.Direction );
      }
      if( implicitSortDescriptions != null )
      {
        propertyNames = propertyNames.Concat( from e in implicitSortDescriptions
                                              select e.PropertyName );
        sortDirections = sortDirections.Concat( from e in implicitSortDescriptions
                                                select e.Direction );
      }

      return QueryableExtensions.OrderBy( queryable,
        propertyNames.ToArray(),
        sortDirections.ToArray(),
        reverse );
    }

    private static IQueryable OrderBy(
      this IQueryable queryable,
      string[] propertyNames,
      ListSortDirection[] sortDirections,
      bool reverse )
    {
      if( queryable == null )
        throw new ArgumentNullException( "queryable" );

      if( ( propertyNames == null ) || ( sortDirections == null ) )
        return queryable;

      if( propertyNames.Length != sortDirections.Length )
        throw new InvalidOperationException( "The propertyNames array must have the same length as the sortDirectionArray." );

      Expression orderedExpression = queryable.Expression;

      Type queryableElementType = queryable.ElementType;

      ParameterExpression[] parameters = new ParameterExpression[] { queryable.CreateParameterExpression() };

      string ascendingOrderByMethodName = "OrderBy";
      string descendingOrderByMethodName = "OrderByDescending";

      for( int i = 0; i < propertyNames.Length; i++ )
      {
        MemberExpression sortMemberExpression = QueryableExtensions.GenerateMemberExpression( parameters[ 0 ], propertyNames[ i ] );

        string methodNameToUse;

        if( ( sortDirections[ i ] == ListSortDirection.Ascending && !reverse ) || ( sortDirections[ i ] == ListSortDirection.Descending && reverse ) )
        {
          methodNameToUse = ascendingOrderByMethodName;
        }
        else
        {
          methodNameToUse = descendingOrderByMethodName;
        }

        orderedExpression = Expression.Call( typeof( Queryable ),
          methodNameToUse,
          new Type[] { queryableElementType, sortMemberExpression.Type },
          orderedExpression, Expression.Quote( Expression.Lambda( sortMemberExpression, parameters ) ) );


        ascendingOrderByMethodName = "ThenBy";
        descendingOrderByMethodName = "ThenByDescending";
      }
      return queryable.Provider.CreateQuery( orderedExpression );
    }

    #endregion ORDERING

    #region FILTERING

    internal static Expression CreateStartsWithExpression(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      params string[] values )
    {
      return QueryableExtensions.FilterString( queryable, sharedParameterExpression, propertyName, values, StringFilterMode.StartsWith );
    }

    internal static Expression CreateEndsWithExpression(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      params string[] values )
    {
      return QueryableExtensions.FilterString( queryable, sharedParameterExpression, propertyName, values, StringFilterMode.EndsWith );
    }

    internal static Expression CreateContainsExpression(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      params string[] values )
    {
      return QueryableExtensions.FilterString( queryable, sharedParameterExpression, propertyName, values, StringFilterMode.Contains );
    }


    internal static Expression CreateGreaterThanExpression(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      params object[] values )
    {
      return QueryableExtensions.CreateBinaryComparison(
        queryable,
        sharedParameterExpression,
        propertyName,
        values,
        BinaryExpression.GreaterThan );
    }

    internal static Expression CreateGreaterThanOrEqualExpression(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      params object[] values )
    {
      return QueryableExtensions.CreateBinaryComparison(
        queryable,
        sharedParameterExpression,
        propertyName,
        values,
        BinaryExpression.GreaterThanOrEqual );
    }


    internal static Expression CreateLesserThanExpression(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      params object[] values )
    {
      return QueryableExtensions.CreateBinaryComparison(
        queryable,
        sharedParameterExpression,
        propertyName,
        values,
        BinaryExpression.LessThan );
    }

    internal static Expression CreateLesserThanOrEqualExpression(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      params object[] values )
    {
      return QueryableExtensions.CreateBinaryComparison(
        queryable,
        sharedParameterExpression,
        propertyName,
        values,
        BinaryExpression.LessThanOrEqual );
    }

    internal static Expression CreateEqualExpression(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      params object[] values )
    {
      return QueryableExtensions.CreateBinaryComparison(
        queryable,
        sharedParameterExpression,
        propertyName,
        values,
        BinaryExpression.Equal );
    }

    internal static Expression CreateDifferentThanExpression(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      params object[] values )
    {
      return QueryableExtensions.CreateBinaryComparison(
        queryable,
        sharedParameterExpression,
        propertyName,
        values,
        BinaryExpression.NotEqual );
    }


    internal static ParameterExpression CreateParameterExpression( this IQueryable queryable )
    {
      if( queryable == null )
        throw new ArgumentNullException( "queryable" );

      return Expression.Parameter( queryable.ElementType, queryable.ElementType.Name.ToLower() );
    }

    internal static Expression CreateBinaryComparison(
      this IQueryable queryable,
      ParameterExpression sharedParameterExpression,
      string propertyName,
      object[] values,
      BinaryComparisonDelegate binaryComparisonDelegate )
    {
      if( queryable == null )
        throw new ArgumentNullException( "queryable" );

      if( ( values == null ) || ( values.Length == 0 ) )
        throw new ArgumentException( "values cannot be null or empty.", "values" );

      if( binaryComparisonDelegate == null )
        throw new ArgumentNullException( "binaryComparisonDelegate" );

      MemberExpression memberExpression = QueryableExtensions.GenerateMemberExpression( sharedParameterExpression, propertyName );

      Expression mergedFilterExpression = null;

      for( int i = 0; i < values.Length; i++ )
      {
        ConstantExpression valueExpression = Expression.Constant( values[ i ] );

        Expression newFilterExpression = binaryComparisonDelegate( memberExpression, Expression.Convert( valueExpression, memberExpression.Type ) );

        if( mergedFilterExpression == null )
        {
          mergedFilterExpression = newFilterExpression;
        }
        else
        {
          mergedFilterExpression = Expression.Or( mergedFilterExpression, newFilterExpression );
        }
      }

      return mergedFilterExpression;
    }

    internal static IQueryable Where( this IQueryable source, Expression predicate )
    {
      if( source == null )
        throw new ArgumentNullException( "source" );

      MethodCallExpression methodCall =
        Expression.Call(
          typeof( Queryable ),
          "Where",
          new Type[] { source.ElementType },
          source.Expression,
          predicate );

      return source.Provider.CreateQuery( methodCall );
    }

    #endregion FILTERING

    #region SELECTING

    internal static IQueryable Select( this IQueryable queryable, string propertyName )
    {
      if( queryable == null )
        throw new ArgumentNullException( "queryable" );

      if( string.IsNullOrEmpty( propertyName ) )
        throw new ArgumentException( "A property name must be provided.", "propertyName" );

      Type queryableElementType = queryable.ElementType;
      ParameterExpression[] parameters = new ParameterExpression[] { Expression.Parameter( queryableElementType, "" ) };

      MemberExpression selectMemberExpression = QueryableExtensions.GenerateMemberExpression( parameters[ 0 ], propertyName );

      LambdaExpression selectLambdaExpression = Expression.Lambda( selectMemberExpression, parameters );

      return queryable.Provider.CreateQuery(
        Expression.Call( typeof( Queryable ), "Select",
        new Type[] { queryableElementType, selectLambdaExpression.Body.Type },
        queryable.Expression, Expression.Quote( selectLambdaExpression ) ) );
    }

    internal static IQueryable GetSubGroupsAndCountsQueryable( this IQueryable queryable, string subGroupBy, bool sortGroupBy, ListSortDirection direction )
    {
      // Create GroupBy

      Type queryableElementType = queryable.ElementType;

      ParameterExpression[] parameters = new ParameterExpression[] { Expression.Parameter( queryableElementType, "" ) };

      MemberExpression memberExpression = QueryableExtensions.GenerateMemberExpression( parameters[ 0 ], subGroupBy );

      LambdaExpression groupByLambdaExpression = Expression.Lambda( memberExpression, parameters );

      MethodCallExpression groupByMethodExpression =
        Expression.Call(
        typeof( Queryable ), "GroupBy",
        new Type[] { queryableElementType, groupByLambdaExpression.Body.Type },
        new Expression[] { queryable.Expression, Expression.Quote( groupByLambdaExpression ) } );

      IQueryable groupedResult = queryable.Provider.CreateQuery( groupByMethodExpression );

      if( sortGroupBy )
        groupedResult = groupedResult.OrderByKey( direction == ListSortDirection.Ascending );

      // Create Select

      ParameterExpression[] groupedParameters = new ParameterExpression[] { Expression.Parameter( groupedResult.ElementType, "" ) };

      UnaryExpression selectKeyExpression = Expression.Convert( Expression.Property( groupedParameters[ 0 ], "Key" ), typeof( object ) );
      UnaryExpression selectCountExpression = Expression.Convert( Expression.Call( typeof( Enumerable ), "Count", new Type[] { queryableElementType }, groupedParameters ), typeof( object ) );


      LambdaExpression finalLambdaExpression = Expression.Lambda(
        Expression.NewArrayInit( typeof( object ), new Expression[] { selectKeyExpression, selectCountExpression } ), groupedParameters );


      MethodCallExpression finalSelectExpression =
        Expression.Call(
        typeof( Queryable ), "Select",
        new Type[] { groupedResult.ElementType, finalLambdaExpression.Body.Type },
        new Expression[] { groupedResult.Expression, Expression.Quote( finalLambdaExpression ) } );


      return groupedResult.Provider.CreateQuery( finalSelectExpression );
    }

    internal static IQueryable Skip( this IQueryable source, int count )
    {
      if( source == null )
        throw new ArgumentNullException( "source" );

      if( count == 0 )
        return source;

      MethodCallExpression methodCall =
        Expression.Call(
          typeof( Queryable ),
          "Skip",
          new Type[] { source.ElementType },
          source.Expression,
          Expression.Constant( count ) );

      return source.Provider.CreateQuery( methodCall );
    }

    internal static IQueryable Take( this IQueryable source, int count )
    {
      if( source == null )
        throw new ArgumentNullException( "source" );

      MethodCallExpression methodCall =
        Expression.Call(
          typeof( Queryable ),
          "Take",
          new Type[] { source.ElementType },
          source.Expression,
          Expression.Constant( count ) );

      return source.Provider.CreateQuery( methodCall );
    }

    internal static int Count( this IQueryable queryable )
    {
      if( queryable == null )
        throw new ArgumentNullException( "queryable" );

      Type queryableElementType = queryable.ElementType;
      Expression expressionToCount = queryable.Expression;

      MethodCallExpression countExpression = Expression.Call(
        typeof( Queryable ), "Count",
        new Type[] { queryableElementType }, expressionToCount );

      int count = ( int )queryable.Provider.Execute( countExpression );
      return count;
    }

    #endregion SELECTING

    #region PRIVATE METHODS

    private static IQueryable OrderByKey( this IQueryable queryable, bool ascending )
    {
      if( queryable == null )
        throw new ArgumentNullException( "queryable" );

      Expression orderedExpression = queryable.Expression;

      Type queryableElementType = queryable.ElementType;

      ParameterExpression parameter = queryable.CreateParameterExpression();

      MemberExpression sortMemberExpression = QueryableExtensions.GenerateMemberExpression( parameter, "Key" );

      string methodName = ( ascending ) ? "OrderBy" : "OrderByDescending";

      orderedExpression = Expression.Call( typeof( Queryable ),
            methodName,
            new Type[] { queryableElementType, sortMemberExpression.Type },
            orderedExpression, Expression.Quote( Expression.Lambda( sortMemberExpression, parameter ) ) );

      return queryable.Provider.CreateQuery( orderedExpression );
    }


    internal static MemberExpression GenerateMemberExpression( ParameterExpression typeExpression, string name )
    {
      if( string.IsNullOrEmpty( name ) )
        throw new ArgumentException( "A name must be provided.", "name" );

      MemberExpression memberExpression = null;

      MemberInfo member = QueryableExtensions.GetMemberInfo( typeExpression.Type, name );

      if( member == null )
        throw new InvalidOperationException( "Unable to generate the MemberExpression." );

      if( member is PropertyInfo )
      {
        // Member is a property.
        memberExpression = Expression.Property( typeExpression, ( PropertyInfo )member );
      }
      else
      {
        // Member is a field.
        memberExpression = Expression.Field( typeExpression, ( FieldInfo )member );
      }

      return memberExpression;
    }

    private static string GetMethodNameFromStringFilterMode( StringFilterMode stringFilterMode )
    {
      switch( stringFilterMode )
      {
        case StringFilterMode.StartsWith:
          {
            return "StartsWith";
          }

        case StringFilterMode.Contains:
          {
            return "Contains";
          }

        case StringFilterMode.EndsWith:
          {
            return "EndsWith";
          }
      }

      return string.Empty;
    }

    private static Expression FilterString(
     this IQueryable queryable,
     ParameterExpression sharedParameterExpression,
     string propertyName,
     string[] values,
     StringFilterMode stringFilterMode )
    {
      if( queryable == null )
        throw new ArgumentNullException( "queryable" );

      if( ( values == null ) || ( values.Length == 0 ) )
        throw new ArgumentException( "values cannot be null or empty.", "values" );

      string methodName = QueryableExtensions.GetMethodNameFromStringFilterMode( stringFilterMode );

      Debug.Assert( !String.IsNullOrEmpty( methodName ) );

      MemberExpression memberExpression = QueryableExtensions.GenerateMemberExpression( sharedParameterExpression, propertyName );

      Expression mergedFilterCall = null;

      for( int i = 0; i < values.Length; i++ )
      {
        ConstantExpression valueExpression = Expression.Constant( values[ i ] );

        MethodCallExpression newFilterCall = Expression.Call(
          memberExpression,
          typeof( string ).GetMethod( methodName, new Type[] { typeof( string ) } ),
          Expression.Convert( valueExpression, memberExpression.Type ) );

        if( mergedFilterCall == null )
        {
          mergedFilterCall = newFilterCall;
        }
        else
        {
          mergedFilterCall = MethodCallExpression.Or( mergedFilterCall, newFilterCall );
        }
      }

      return mergedFilterCall;
    }

    #endregion PRIVATE METHODS

    #region INTERNAL FIELDS

    internal delegate Expression BinaryComparisonDelegate( Expression left, Expression right );




    #endregion INTERNAL FIELDS

    #region PRIVATE NESTED ENUMS

    private enum StringFilterMode
    {
      StartsWith = 0,
      Contains = 1,
      EndsWith = 2
    }

    #endregion PRIVATE NESTED ENUMS
  }
}
